package main

import (
	"google.golang.org/appengine"
	"google.golang.org/appengine/memcache"
	"github.com/labstack/echo"
	"net/http"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"encoding/json"
	"cloud.google.com/go/storage"
	"io/ioutil"
	"sync/atomic"
	"io"
	"html/template"
	"time"
	"strconv"
	_ "github.com/valyala/bytebufferpool"
	"context"
	"fmt"
	"strings"
	"github.com/tidwall/gjson"
	"google.golang.org/appengine/urlfetch"
)

type JSON map[string]interface{}

type DataItem struct {
	RemoteAddr string
	Count      int64
}

type WeiBoNews struct {
	Title     string `json:"title"`
	Content   string `json:"content" datastore:",noindex"`
	WeiboId   string `json:"weibo_id"`
	Created   int64  `json:"created"`
	PicsJson  string `json:"pics_json" datastore:",noindex"`
	MediaInfo string `json:"media_info" datastore:",noindex"`
}

type Template struct {
	templates *template.Template
}

func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
	return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
	var counter int64

	t := &Template{
		templates: template.Must(template.ParseGlob("views/*.html")),
	}
	e := echo.New()
	e.Renderer = t
	e.GET("/tpl", func(c echo.Context) error {
		return c.Render(200, "hello", "ni hao")
	})
	e.POST("/add_news", func(c echo.Context) error {
		ctx := appengine.NewContext(c.Request())
		key := datastore.NewIncompleteKey(ctx, "WeiBoNews", nil)
		news := &WeiBoNews{
			Title:     c.FormValue("news_title"),
			Content:   c.FormValue("content"),
			WeiboId:   c.FormValue("weibo_id"),
			PicsJson:  c.FormValue("pics_json"),
			MediaInfo: c.FormValue("media_info"),
			Created:   time.Now().UnixNano(),
		}
		key, err := datastore.Put(ctx, key, news)
		if err != nil {
			log.Errorf(ctx, "can't put WeiBoNews: %s, title: %s, content: %s",
				news.Title, news.Content, err.Error())
			return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
		}
		memcache.Delete(ctx, "WeiBoCacheNamespace")
		return c.JSON(200, JSON{"ok": "yes", "key_intID": key.IntID(), "key_stringID": key.StringID()})
	})
	e.GET("/get_news", func(c echo.Context) error {
		ctx := appengine.NewContext(c.Request())
		limit, _ := strconv.Atoi(c.QueryParam("limit"))
		offset, _ := strconv.Atoi(c.QueryParam("offset"))
		if limit <= 0 {
			limit = 10
		}
		if offset < 0 {
			offset = 0
		}
		var namespace string
		c.Response().Header().Set("Access-Control-Allow-Origin", "*")
	FlagBegin:
		if item, err := memcache.Get(ctx, "WeiBoCacheNamespace"); err != nil {
			if err == memcache.ErrCacheMiss {
				namespace = strconv.FormatInt(time.Now().UnixNano(), 10)
				item := &memcache.Item{Key: "WeiBoCacheNamespace", Value: []byte(namespace)}
				if err = memcache.Add(ctx, item); err != nil {
					// 竞争情况下同时 Add
					// Add 失败者重试 其他错误直接返回
					if err == memcache.ErrNotStored {
						goto FlagBegin
					}
					return c.JSON(500, JSON{"ok": "no", "position": "memcache.Add", "err": err.Error()})
				} else {
					newsList, err := getWeiBoNewsList(ctx, namespace, limit, offset)
					if err != nil {
						return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
					}
					return c.JSON(200, JSON{"ok": "yes", "data": newsList})
				}
			} else {
				// 未知错误
				return c.JSON(500, JSON{"ok": "no", "position": "memcache.Get", "err": err.Error()})
			}
		} else {
			// namespace 存在
			namespace = string(item.Value)
			newsList, err := getWeiBoNewsList(ctx, namespace, limit, offset)
			if err != nil {
				return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
			}
			return c.JSON(200, JSON{"ok": "yes", "data": newsList})
		}

	})
	e.GET("/get_page_info", func(c echo.Context) error {
		ctx := appengine.NewContext(c.Request())
		c.Response().Header().Set("Access-Control-Allow-Origin", "*")
		weiboJson, err := GetWeiboJsonById(ctx, c.QueryParam("weibo_id"))
		if err != nil {
			log.Errorf(ctx, "GetWeiboJsonById: %s", err.Error())
		}
		pageInfo := gjson.Get(weiboJson, "status.page_info")
		if pageInfo.Exists() {
			return c.JSONBlob(200, []byte(pageInfo.String()))
		} else {
			return c.JSON(500, JSON{"ok": "no", "err": "can't get page_info"})
		}
	})
	e.GET("/", func(c echo.Context) error {
		//ctx := appengine.NewContext(c.Request())
		//remoteAddr := "my-key"
		//if newValue, err := memcache.Increment(ctx, remoteAddr, 1, 0); err != nil {
		//	return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
		//} else {
		//	dataItem := &DataItem{RemoteAddr: c.Request().RemoteAddr, Count: int64(newValue)}
		//	key := datastore.NewIncompleteKey(ctx, "DataItem", nil)
		//	if _, err := datastore.Put(ctx, key, dataItem); err != nil {
		//		log.Errorf(ctx, "can't put dataItem remoteAddr: %s, count: %d, err: %s",
		//			remoteAddr, newValue, err.Error())
		//	}
		//	memcache.Delete(ctx, "visitor")
		//	return c.JSON(200, JSON{"ok": "yes", "data": newValue})
		//}
		ctx := appengine.NewContext(c.Request())
		if err := c.Render(200, "news_list.html", nil); err != nil {
			log.Errorf(ctx, "error: %s", err)
		}
		return nil
	})
	e.GET("/get", func(c echo.Context) error {
		ctx := appengine.NewContext(c.Request())
		if item, err := memcache.Get(ctx, "visitor"); err != nil {
			if err == memcache.ErrCacheMiss {
				q := datastore.NewQuery("DataItem")
				var dataItemList []DataItem
				if _, err := q.GetAll(ctx, &dataItemList); err != nil {
					return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
				}
				item = &memcache.Item{Key: "visitor"}
				if item.Value, err = json.Marshal(dataItemList); err != nil {
					return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
				}
				if err = memcache.Add(ctx, item); err != nil {
					return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
				}
				if err := json.Unmarshal(item.Value, &dataItemList); err != nil {
					return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
				}
				return c.JSON(200, JSON{"ok": "yes", "data": dataItemList})
			} else {
				return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
			}
		} else {
			var dataItemList []DataItem
			if err := json.Unmarshal(item.Value, &dataItemList); err != nil {
				return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
			}
			return c.JSON(200, JSON{"ok": "yes", "data": dataItemList})
		}
	})
	e.GET("/counter", func(c echo.Context) error {
		return c.JSON(200, JSON{"ok": "yes", "count": atomic.LoadInt64(&counter)})
	})
	e.GET("/save_file", func(c echo.Context) error {
		ctx := appengine.NewContext(c.Request())
		if storageClient, err := storage.NewClient(ctx); err != nil {
			return c.JSON(500, JSON{"ok": "no", "err": err.Error()})
		} else {
			bkt := storageClient.Bucket("blogforpiaoyu.appspot.com")
			obj := bkt.Object("test.txt")
			r, err := obj.NewReader(ctx)
			objWriter := obj.NewWriter(ctx)
			var bufin []byte
			if err != nil {
				if err != storage.ErrObjectNotExist {
					return c.JSON(500, JSON{"ok": "no", "function": "NewReader", "err": err.Error()})
				}
			} else {
				/*File is existed.*/
				defer r.Close()
				if bufin, err = ioutil.ReadAll(r); err != nil {
					return c.JSON(500, JSON{"ok": "no", "function": "ReadAll", "err": err.Error()})
				}
			}
			/*File is NOT existed.*/
			if len(bufin) == 0 {
				bufin = []byte("Init file manually.")
			}
			objWriter.Write(bufin)
			objWriter.Write(bufin)
			objWriter.Write([]byte("\n"))
			if err = objWriter.Close(); err != nil {
				return c.JSON(500, JSON{"ok": "no", "function": "Close", "err": err.Error()})
			}
			r, err = obj.NewReader(ctx)
			if err != nil {
				return c.JSON(500, JSON{"ok": "no", "function": "NewReader", "err": err.Error()})
			} else {
				if buf, err := ioutil.ReadAll(r); err != nil {
					return c.JSON(500, JSON{"ok": "no", "function": "ReadAll", "err": err.Error()})
				} else {
					return c.JSON(200, JSON{"ok": "yes", "data": string(buf), "total": len(buf)})
				}
			}
		}
	})
	http.Handle("/", e)
	appengine.Main()
}

func getWeiBoNewsList(ctx context.Context, namespace string, limit, offset int) (ret []WeiBoNews, err error) {
	memcacheKey := fmt.Sprintf("%s-%d-%d", namespace, limit, offset)
	item, err := memcache.Get(ctx, memcacheKey)
	if err != nil {
		if err == memcache.ErrCacheMiss {
			// 缓存未命中
			log.Infof(ctx, "cache miss")
			q := datastore.NewQuery("WeiBoNews").Limit(limit).Offset(offset).Order("-Created")
			if _, err = q.GetAll(ctx, &ret); err != nil {
				return nil, err
			}
			itemBytes, err := json.Marshal(ret)
			if err != nil {
				return nil, err
			}
			item = &memcache.Item{Key: memcacheKey, Value: itemBytes}
			memcache.Add(ctx, item)
			return ret, nil
		} else {
			// 未知错误
			return
		}
	}
	// 缓存命中
	log.Infof(ctx, "cache hit")
	if err = json.Unmarshal(item.Value, &ret); err != nil {
		return nil, err
	}
	return
}

func GetWeiboJsonById(ctx context.Context, weiboId string) (ret string, err error) {
	client := urlfetch.Client(ctx)
	resp, err := client.Get("https://m.weibo.cn/status/" + weiboId)
	if err != nil {
		fmt.Errorf("http.Get: %s", err.Error())
		return ret, err
	}
	bytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Errorf("ioutil.ReadAll: %s", err.Error())
		return ret, err
	}
	bodyStr := string(bytes)
	beginStr := "var $render_data = ["
	begin := strings.Index(bodyStr, beginStr)
	end := strings.Index(bodyStr, "[0] || {};")
	if begin == -1 || end == -1 {
		fmt.Errorf("can't find begin or end flag")
		return ret, err
	}
	ret = bodyStr[begin+len(beginStr) : end-1]
	fmt.Println(ret)
	return ret, nil
}
